# coding: utf-8
""" 一些全局方法，默认配置等，main里面不用配置的代码转移到本文件 """
from flask import Flask, send_from_directory, Blueprint, request as flask_request
from werkzeug import exceptions
import traceback
from plugin import Mylogger, MyResponse, MyException
from flask_docs import ApiDoc
from config import Config, get_packed_support_path
from flask_cors import CORS
import requests

app = Flask(__name__)

# 解决flask中文乱码的问题，将json数据内的中文正常显示
app.config['JSON_AS_ASCII'] = False
# 接口文档初始化
ApiDoc(app, Config.config_apidoc.apidoc.title, Config.config_apidoc.apidoc.version)


# 配置图标文件，防止报错，浏览器直接请求接口时会自动请求favicon.ico
@app.route('/favicon.ico')
def favicon():
    return send_from_directory(get_packed_support_path('view/resources'), 'favicon.ico')


# 注册蓝图，并跨域
def register_blueprints():
    """ 自定义蓝图的统一注册和跨域,默认url_prefix为蓝图名 """
    # app.config['BluePrints'].append(sche)
    # app.config['API_DOC_MEMBER'].append(sche)
    host = '127.0.0.1' if Config.module_config.host == '0.0.0.0' else Config.module_config.host
    Mylogger.info("访问http://{}:{}/docs/api 接口查看文档".format(host, Config.module_config.port))
    app.config['BluePrints'] = app.config.get('BluePrints', [])
    app.config['API_DOC_MEMBER'] = app.config.get('API_DOC_MEMBER', [])
    app.config['API_DOC_TITLE'] = app.config.get('API_DOC_TITLE', [])
    for item in app.config['BluePrints']:
        if type(item) == tuple:
            # 如果item是(bp,url)
            CORS(item[0])
            app.register_blueprint(item[0], url_prefix=item[1])
        else:  # 如果item是bp
            CORS(item)
            if not item.url_prefix:
                item.url_prefix = '/' + item.name
            app.register_blueprint(item)


def ErrorHandler():
    '''
    flask全局异常处理
    '''
    try:

        @app.errorhandler(Exception)
        def errorhandler(e):
            Mylogger.error(e)
            Mylogger.error("*********************************")
            if e.code not in (404, ):
                Mylogger.error(traceback.format_exc())
            if isinstance(e, exceptions.HTTPException):
                return MyResponse.Error((e.code, e.description))
            else:
                return MyResponse.Error(MyException.ERROR_UNKNOW)
    except Exception as e:
        Mylogger.error(e)
        Mylogger.error("*********************************")
        Mylogger.error(traceback.format_exc())
        return MyResponse.Error(MyException.ERROR_UNKNOW)


ErrorHandler()

################# requests时可保存请求记录 #################
log_enable = Config.config.env.save_api_invoke.log or False
log_detail_enable = Config.config.env.save_api_invoke.log_detail or False
sqlite_enable = Config.config.env.save_api_invoke.sqlite or False
separate_type = Config.config.env.save_api_invoke.separate_type or 0

# region 保存请求记录


# requests.request方法重构
def _request(method, url, logger = Mylogger, **kwargs):
    """Constructs and sends a :class:`Request <Request>`.

    :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
    :param url: URL for the new :class:`Request` object.
    :param params: (optional) Dictionary, list of tuples or bytes to send
        in the query string for the :class:`Request`.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
    :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
    :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
    :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
        ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
        or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
        defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
        to add for the file.
    :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
    :param timeout: (optional) How many seconds to wait for the server to send data
        before giving up, as a float, or a :ref:`(connect timeout, read
        timeout) <timeouts>` tuple.
    :type timeout: float or tuple
    :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
    :type allow_redirects: bool
    :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
    :param verify: (optional) Either a boolean, in which case it controls whether we verify
            the server's TLS certificate, or a string, in which case it must be a path
            to a CA bundle to use. Defaults to ``True``.
    :param stream: (optional) if ``False``, the response content will be immediately downloaded.
    :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response

    Usage::

      >>> import requests
      >>> req = requests.request('GET', 'https://httpbin.org/get')
      >>> req
      <Response [200]>
    """

    # By using the 'with' statement we are sure the session is closed, thus we
    # avoid leaving sockets open which can trigger a ResourceWarning in some
    # cases, and look like a memory leak in others
    from requests import sessions
    with sessions.Session() as session:
        if log_enable and logger:
            logger.debug(f'[Request] {method} {url} {kwargs}')
        if sqlite_enable:
            pass
        response = session.request(method=method, url=url, **kwargs)
        if log_enable and logger:
            if log_detail_enable:
                Mylogger.debug(f'[Responsed] {response.text}')
            else:
                Mylogger.debug(f'[Responsed] {response.status_code}')
        if sqlite_enable:
            pass
        return response


def _get(url, params=None, **kwargs):
    r"""Sends a GET request.

    :param url: URL for the new :class:`Request` object.
    :param params: (optional) Dictionary, list of tuples or bytes to send
        in the query string for the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return _request("get", url, params=params, **kwargs)


def _options(url, **kwargs):
    r"""Sends an OPTIONS request.

    :param url: URL for the new :class:`Request` object.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return _request("options", url, **kwargs)


def _head(url, **kwargs):
    r"""Sends a HEAD request.

    :param url: URL for the new :class:`Request` object.
    :param \*\*kwargs: Optional arguments that ``request`` takes. If
        `allow_redirects` is not provided, it will be set to `False` (as
        opposed to the default :meth:`request` behavior).
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    kwargs.setdefault("allow_redirects", False)
    return _request("head", url, **kwargs)


def _post(url, data=None, json=None, **kwargs):
    r"""Sends a POST request.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) json data to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return _request("post", url, data=data, json=json, **kwargs)


def _put(url, data=None, **kwargs):
    r"""Sends a PUT request.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) json data to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return _request("put", url, data=data, **kwargs)


def _patch(url, data=None, **kwargs):
    r"""Sends a PATCH request.

    :param url: URL for the new :class:`Request` object.
    :param data: (optional) Dictionary, list of tuples, bytes, or file-like
        object to send in the body of the :class:`Request`.
    :param json: (optional) json data to send in the body of the :class:`Request`.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return _request("patch", url, data=data, **kwargs)


def _delete(url, **kwargs):
    r"""Sends a DELETE request.

    :param url: URL for the new :class:`Request` object.
    :param \*\*kwargs: Optional arguments that ``request`` takes.
    :return: :class:`Response <Response>` object
    :rtype: requests.Response
    """

    return _request("delete", url, **kwargs)


requests.request = _request
requests.get = _get
requests.options = _options
requests.head = _head
requests.post = _post
requests.put = _put
requests.patch = _patch
requests.delete = _delete

# endregion

############################## 外部请求本程序接口 ##############################

# region 外部请求本程序接口


def _before_request(logger=Mylogger, log_enable=log_enable, sqlite_enable=sqlite_enable):
    """ 记录请求信息 """
    if log_enable:
        logger.debug("[Requested] {} {}".format(flask_request.method, flask_request.url))
        # logger.info(request.headers)
        # logger.info(request.data)
        # logger.info(request.form)
        # logger.info(request.args)
        # logger.info(request.json)
        # logger.info(request.files)
    if sqlite_enable:
        pass


def _after_request(response, logger=Mylogger):
    """ 记录响应信息 """
    if 'text/html' in response.mimetype:
        if log_enable:
            logger.debug('[Response] {}'.format(response.data.decode('utf-8')[:1024]))  # 只记录前1024个字符
            # logger.info(response.data)
            # 'image/x-icon'
            # 'text/html; charset=utf-8'
            # 'application/octet-stream'
        if sqlite_enable:
            pass
    else:
        if log_enable:
            logger.debug("[{}] 返回资源类型：{}".format(response.status, response.mimetype))
        if sqlite_enable:
            pass
    return response


# 全局请求钩子，不区分蓝图时使用该钩子函数，所有请求都会记录到同一个sqlite表或同一个日志文件
if not separate_type:

    @app.before_request
    def before_request():
        _before_request()

    @app.after_request
    def after_request(response):
        return _after_request(response)


# 蓝图钩子,如果希望不同蓝图记录到不同sqlite表或不同日志文件，调用该钩子函数
def before_request_for_blueprints(blueprints: list[Blueprint], logger=Mylogger):
    """ 注册蓝图的请求钩子 """
    if separate_type:
        logger = logger or Mylogger
        for blueprint in blueprints:

            @blueprint.before_request
            def before_request():
                _before_request(logger)


def after_request_for_blueprints(blueprints: list[Blueprint], logger=Mylogger):
    """ 注册蓝图的响应钩子 """
    if separate_type:
        logger = logger or Mylogger
        for blueprint in blueprints:

            @blueprint.after_request
            def after_request(response):
                return _after_request(response, logger)


# endregion

############################## 程序退出时记录最后状态值 ##############################

from plugin import Sqlite, SqliteIndep
import atexit

MY_STORED_CACHE = {}  # 用于存储程序退出时需要记录的状态值


def INIT_MY_STORED_CACHE():
    """ 初始化状态值 """
    global MY_STORED_CACHE
    db = Sqlite()
    database = db.select("SELECT name FROM sqlite_master WHERE name='MY_STORED_CACHE'")
    if len(database) != 0:
        datas = db.select("SELECT key,value FROM MY_STORED_CACHE")
        if datas and len(datas) > 0:
            for data in datas:
                MY_STORED_CACHE[data[0]] = data[1]


INIT_MY_STORED_CACHE()


import threading
lock = threading.Lock()

def UPDATE_MY_STORED_CACHE(key, value):
    """ 更新状态值,并记录到数据库 """
    global MY_STORED_CACHE
    with lock:
        Mylogger.info(f"更新状态值：{key}={value}")
        db = SqliteIndep()
        MY_STORED_CACHE[key] = value
        db.execute("CREATE TABLE IF NOT EXISTS MY_STORED_CACHE (key varchar(1024),value text)")
        db.execute(f"DELETE FROM MY_STORED_CACHE WHERE key='{key}'")
        db.execute(f"INSERT INTO MY_STORED_CACHE VALUES ('{key}','{value}')")


@atexit.register
def RECORD_MY_STORED_CACHE():
    """ 程序退出前，记录状态值 """
    _record_my_stored_cache()


def _record_my_stored_cache():
    """ 记录状态值 """
    global MY_STORED_CACHE
    try:
        db = Sqlite()
        db.execute("CREATE TABLE IF NOT EXISTS MY_STORED_CACHE (key varchar(1024),value text)")
        db.execute("DELETE FROM MY_STORED_CACHE")
        # 一次性插入多条数据
        if len(MY_STORED_CACHE) > 0:
            # ('k','v')
            values = ["('{}','{}')".format(k, v) for k, v in MY_STORED_CACHE.items()]
            db.execute("INSERT INTO MY_STORED_CACHE VALUES {}".format(','.join(values)))
            Mylogger.info("程序退出，记录状态值成功")
            # db.executemany("INSERT INTO MY_STORED_CACHE VALUES (?,?)", [(k, v) for k, v in MY_STORED_CACHE.items()])
    except Exception as e:
        Mylogger.error("程序退出，记录状态值失败")
        Mylogger.error(e)
